package demo;

import com.betfair.event.FixtureInfo;
import com.betfair.wrapper.types.Event;
import com.betfair.wrapper.types.MarketPrice;
import com.betfair.wrapper.types.MarketSummaryWrap;
import com.betfair.wrapper.types.MarketWrap;
import com.betfair.www.publicapi.types.exchange.v5.*;
import com.betfair.www.publicapi.types.global.v3.BFEvent;
import com.betfair.www.publicapi.types.global.v3.EventType;
import com.betfair.www.publicapi.types.global.v3.GetEventsResp;
import com.betfair.www.publicapi.types.global.v3.MarketSummary;
import demo.handler.ExchangeAPI;
import demo.handler.ExchangeAPI.Exchange;
import demo.handler.GlobalAPI;
import demo.util.*;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

import java.text.SimpleDateFormat;
import java.util.*;


/**
 * Demonstration of the Betfair API.
 * <p/>
 * This is the main control class for running the Betfair API demo.
 * User display and input is handled by the Display class
 * API Management is handled by the classes in the apihandler package
 */
public class APIDemo {

    // Menus
    private static final String[] MAIN_MENU = new String[]
            {"View account", "Choose Market", "View Market", "View Complete Market", "Bet Management", "View Usage", "Exit"};
    private static final String[] BETS_MENU = new String[]
            {"Place Bet", "Update Bet", "Cancel Bet", "Back"};
    // The session token
    private static APIContext apiContext = new APIContext();
    // the current chosen market and Exchange for that market
    private static Market selectedMarket;
    private static Exchange selectedExchange;
    private static SessionFactory sessionFactory;

    // Fire up the API demo
    public static void main(String[] args) throws Exception {

        // Initialise logging and turn logging off. Change OFF to DEBUG for detailed output.
        Logger rootLog = LogManager.getRootLogger();
        Level lev = Level.toLevel("OFF");
        rootLog.setLevel(lev);

        Display.println("Welcome to the Betfair API Demo");
        String username = args.length < 1 ? Display.getStringAnswer("Betfair username:") : args[0];
        String password = args.length < 2 ? Display.getStringAnswer("Betfair password:") : args[1];

        // Perform the login before anything else.
        try {
            GlobalAPI.login(apiContext, username, password);
        } catch (Exception e) {
            // If we can't log in for any reason, just exit.
            Display.showException("*** Failed to log in", e);
            System.exit(1);
        }

        boolean finished = false;

        while (!finished) {
            try {
                int choice = Display.getChoiceAnswer("\nChoose an operation", MAIN_MENU);
                switch (choice) {
                    case 0: // View account
                        showAccountFunds(Exchange.UK);
                        showAccountFunds(Exchange.AUS);
                        break;
                    case 1: // Choose Market
                        chooseMarket();
                        break;
                    case 2: // View Market
                        //viewMarket(e, m, selectedMarket);
                        break;
                    case 3: // Show Complete Market
                        viewCompleteMarket();
                        break;
                    case 4: // Show Bets
                        manageBets();
                        break;
                    case 5: // Show Usage
                        int type = Display.getChoiceAnswer("\nType of stats required", new String[]{"Combined", "Timed"});
                        if (type == 0) {
                            Display.showCombinedUsage(apiContext.getUsage());
                        } else {
                            Display.showTimedUsage(apiContext.getUsage());
                        }
                        break;
                    case 6: // Exit
                        finished = true;
                        break;
                }
            } catch (Exception e) {
                // Print out the exception and carry on.
                Display.showException("*** Failed to call API", e);
            }

        }

        // Logout before shutting down.
        try {
            GlobalAPI.logout(apiContext);
        } catch (Exception e) {
            // If we can't log out for any reason, there's not a lot to do.
            Display.showException("Failed to log out", e);
        }
        Display.println("Logout successful");
    }

    // Check if a market is selected
    private static boolean isMarketSelected() {
        if (selectedMarket == null) {
            Display.println("You must select a market");
            return false;
        }
        return true;
    }

    // Retrieve and display the account funds for the specified exchange
    private static void showAccountFunds(Exchange exch) throws Exception {
        GetAccountFundsResp funds = ExchangeAPI.getAccountFunds(exch, apiContext);
        Display.showFunds(exch, funds);
    }

    public static int eventTypeFilter(EventType[] types, String name) {
        StringBuilder sb = new StringBuilder();
        for (EventType e : types) {
            if (e.getName().equals(name)) {
                return e.getId();
            }
            sb.append(e.getName()).append(";");
        }
        throw new IllegalArgumentException("No event type found for name:" + name + ". Valid Events are:" + sb.toString());
    }

    public static FixtureInfo getNextAvailableFixtureDay(BFEvent[] events) {
        StringBuilder sb = new StringBuilder();
        FixtureInfo fi = new FixtureInfo();
        fi.setEventId(-1);
        int offset = 0;
        if (checkFixtureExists(events)) {
            while (fi.getEventId() < 0 && offset <= Constants.MAX_FIXTURE_DAY) {
                fi.setEventId(eventFilter(events, getFixtureForDayOffset(offset)));
                fi.setFixtureDay(getFixtureForDayOffset(offset));
                if (fi.getEventId() > 0) {
                    Display.println("Using Event found for: " + getFixtureForDayOffset(offset));
                }
                offset++;
            }
        }
        return fi;
    }

    private static boolean checkFixtureExists(BFEvent[] events) {
        StringBuilder sb = new StringBuilder();
        for (BFEvent e : events) {
            if (e.getEventName().contains(Constants.FIXTURES)) {
                return true;
            }
            sb.append(e.getEventName()).append(";");
        }
        Display.println("No event found for name: " + Constants.FIXTURES + ". Valid Events are:" + sb.toString());
        return false;
    }

    public static int eventFilter(BFEvent[] events, String name) {
        StringBuilder sb = new StringBuilder();
        for (BFEvent e : events) {
            if (e.getEventName().contains(name)) {
                return e.getEventId();
            }
            sb.append(e.getEventName()).append(";");
        }
        Display.println("No event found for name: " + name + ". Valid Events are:" + sb.toString());
        ;
        //throw new IllegalArgumentException("No event found for name: " + name + ". Valid Events are:" + sb.toString());
        return -1;
    }

    public static String findDayOffsetFixtureString(int offset) {
        Calendar c = Calendar.getInstance();
        c.add(Calendar.DATE, offset);
        SimpleDateFormat d = new SimpleDateFormat();
        d.applyPattern("dd MMMMM");
        return d.format(c.getTime());
    }

    // Select a market by the following process
    // * Select a type of event
    // * Recursively select an event of this type
    // * Select a market within this event if one exists.
    private static void chooseMarket() throws Exception {
        // Get available event types.
        EventType[] types = GlobalAPI.getActiveEventTypes(apiContext);
        //int typeChoice = Display.getChoiceAnswer("Choose an event type:", types);

        sessionFactory = new Configuration()
                .configure() // configures settings from hibernate.cfg.xml
                .buildSessionFactory();
        BFEvent[] bfEvents = null;
        List<Event> events = new ArrayList<Event>();
        int leagueEventId = 0;
        BFEvent[] leagueEvents = null;

        String sportSelection = Constants.SOCCER;
        String countrySelection = Constants.ENGLISH_SOCCER;
        String leagueSelection = Constants.BARCLAYS_PREMIER_LEAGUE;

        countrySelection = Constants.SPANISH_SOCCER;
        leagueSelection = Constants.PRIMERA_DIVISION;

        countrySelection = Constants.UEFA_CHAMPIONS_LEAGUE;
        leagueSelection = Constants.PRIMERA_DIVISION;
        int maxThrottle = 500;


        int eventId = eventTypeFilter(types, sportSelection);

        // Get available events of this type

        bfEvents = getBfEvents(eventId);
        int countryEventId = eventFilter(bfEvents, countrySelection);
        BFEvent[] countryEvents = getBfEvents(countryEventId);
        if (countrySelection != Constants.UEFA_CHAMPIONS_LEAGUE) {
            leagueEventId = eventFilter(countryEvents, leagueSelection);
            leagueEvents = getBfEvents(leagueEventId);
        } else {
            leagueSelection = "";
            leagueEvents = countryEvents;
        }
        FixtureInfo fixtureInfo = getNextAvailableFixtureDay(leagueEvents);
        bfEvents = getBfEvents(fixtureInfo.getEventId());

        Display.println("Chosen Event Type of: " + countrySelection + "; " + leagueSelection + "; " + fixtureInfo.getFixtureDay());

        Map<String, MarketDetail> interestedMarkets = new HashMap<String, MarketDetail>();

        List<MarketNameEnum> marketFilter = new ArrayList<MarketNameEnum>();
        marketFilter.add(MarketNameEnum.CORRECT_SCORE);
        marketFilter.add(MarketNameEnum.OVER_UNDER_2_5);
        marketFilter.add(MarketNameEnum.OVER_UNDER_4_5);

        SessionFactory sessionFactory = new Configuration()
                .configure() // configures settings from hibernate.cfg.xml
                .buildSessionFactory();

        // now lets pull events from the database and list them
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        String sql = "from Event " +
                "where menuLevel1 = '" + sportSelection + "' " +
                "and menuLevel2 = '" + countrySelection + "' " +
                "and menuLevel3 = '" + leagueSelection + "' " +
                "and menuLevel4 = '" + fixtureInfo.getFixtureDay() + "'";
        Display.println("SQL: " + sql);
        List<Event> result = session.createQuery(sql).list();
        for (Event event : (List<Event>) result) {
            System.out.println("Event (" + event.getEventName() + ") : " + event.getEventId() + " : " + event.getId());
        }
        session.getTransaction().commit();
        session.close();

        for (BFEvent bfEvent : bfEvents) {
            boolean found = false;
            for (Event event : result) {
                if (event.getEventId() == bfEvent.getEventId()) {
                    found = true;
                    events.add(event);
                    break;
                }
            }
            if (!found) {
                Event event = new Event(bfEvent);
                event.setMenuLevel1(sportSelection);
                event.setMenuLevel2(countrySelection);
                event.setMenuLevel3(leagueSelection);
                event.setMenuLevel4(fixtureInfo.getFixtureDay());
                events.add(event);
            }
        }

        constructAndSaveMarketDetail(events, interestedMarkets, marketFilter, maxThrottle);

        saveEvents(events);

        Display.println("");
        Display.println(interestedMarkets.keySet().toString());
        Display.println(interestedMarkets.values().toString());

        findPrices(events);
    }

    private static void findPrices(List<Event> events) throws Exception {
        for (int i = 0; i < 30; i++) {
            for (Event event : events) {
                for (MarketSummaryWrap marketSummary : event.getMarketSummary()) {
                    MarketWrap market = event.getMarket(marketSummary.getMarketId());
                    //if (marketSummary.getBetDelay() > 0 && marketSummary.getMarketName().startsWith(MarketNameEnum.CORRECT_SCORE.getCode())) {
                    Exchange exchange = marketSummary.getExchangeId() == 1 ? Exchange.UK : Exchange.AUS;
                    InflatedMarketPrices prices = ExchangeAPI.getMarketPrices(exchange, apiContext, marketSummary.getMarketId());
                    if (market.getMarketPrice() == null) {
                        MarketPrice marketPrice = new MarketPrice(prices);
                        marketPrice.setMarket(market);
                        market.setMarketPrice(marketPrice);
                    } else {
                        market.getMarketPrice().setPrices(prices);
                    }
                }
            }
            saveEvents(events);
            Display.println("...");
            Thread.sleep(5000);
        }
    }

    private static void findPricesOld(Map<String, MarketDetail> interestedMarkets) throws Exception {
        for (int i = 0; i < 30; i++) {
            for (MarketDetail marketDetail : interestedMarkets.values()) {
                if (marketDetail.getMarketSummary().getBetDelay() > 0 && marketDetail.getMarketName().startsWith(MarketNameEnum.CORRECT_SCORE.getCode())) {
                    //Display.println("  " + Util.printMarketSummary(marketDetail.getMarketSummary()));
                    InflatedMarketPrices prices = getPrices(marketDetail);

                    //viewMarket(e, marketDetail, prices);
                    viewCorrectScore(marketDetail, prices);
                    int newTotalGoals = CorrectScore.getTotalGoalsInMarket(prices, marketDetail.getMarket());
                    if (i > 0 && newTotalGoals > marketDetail.getCurrentTotalGoals()) {
                        Display.println("GOOOOOOOOAAAAAAAAAALLLLLLL!!!! Place Bets Now!");
                        marketDetail.setCurrentTotalGoals(newTotalGoals);
                    }
                } else {
                    // Display.println("Skipping Market as it is not in Play: " + e.getEventName() + "; " + m.getMarketName());
                }

            }
            Display.println("...");
            Thread.sleep(5000);
        }
    }

    private static void constructAndSaveMarketDetail(List<Event> events, Map<String, MarketDetail> interestedMarkets, List<MarketNameEnum> marketFilter, int maxThrottle) throws Exception {
        int i = 0;
        int j = 0;
        for (Event event : events) {
            HashSet<MarketSummaryWrap> marketSummaries = new HashSet<MarketSummaryWrap>();
            HashSet<MarketWrap> markets = new HashSet<MarketWrap>();
            Market market = null;
            Display.println(event.getEventName());
            MarketSummary[] mkts = getMarketSummaryForEvent(event.getEventId());
            for (MarketWrap marketWrap : event.getMarket()) {
                boolean found = false;
                for (MarketSummary marketSummary : mkts) {
                    if (marketWrap.getMarketId() == marketSummary.getMarketId()) {
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    //Got a Market from the DB that is not coming back from the WebService, must be closed now so remove it.
                    event.getMarket().remove(marketWrap);
                }
            }
            if (i < maxThrottle) {
                for (MarketSummary marketSummary : mkts) {
                    for (MarketWrap marketWrap : event.getMarket()) {
                        if (marketWrap.getMarketId() == marketSummary.getMarketId()) {
                            //got this one already, crack on.
                            continue;
                        }
                    }
                    try {
                        if (marketFilter.contains(MarketNameEnum.fromString(marketSummary.getMarketName()))) {
                            Exchange exchange = marketSummary.getExchangeId() == 1 ? Exchange.UK : Exchange.AUS;
                            market = getMarketWrapped(exchange, apiContext, marketSummary.getMarketId());
                            MarketDetail marketDetail = new MarketDetail(exchange, event, market, marketSummary);
                            interestedMarkets.put(event.getEventName() + "|" + marketSummary.getMarketName(), marketDetail);
                            Display.println("  " + Util.printMarketSummary(marketSummary));
                            MarketSummaryWrap msw = new MarketSummaryWrap(marketSummary);
                            msw.setEvent(event);
                            marketSummaries.add(msw);
                            MarketWrap m = new MarketWrap(market);
                            m.setEvent(event);
                            markets.add(m);
                            j++;
                            Display.println("Number of Calls: " + j);
                        }
                    } catch (IllegalArgumentException ex) {
                        if (ex.toString().contains("API_ERROR") && ex.toString().contains("EXCEEDED_THROTTLE")) {
                            Display.println(ex.toString());
                        }
                        //Do Nothing, just means that the Enum is not fully complete.
                    }
                    i++;
                }
            }
            event.setMarketSummary(marketSummaries);
            event.setMarket(markets);
        }
    }

    private static void saveEvents(List<Event> events) {
        Session session = sessionFactory.openSession();
        session.beginTransaction();
        for (Event event : events) {
            session.saveOrUpdate(event);
        }
        session.getTransaction().commit();
        session.close();
    }

    private static Market getMarketWrapped(Exchange exchange, APIContext apiContext, int marketId) throws Exception {
        UsageMap u = apiContext.getUsage();
        while (u.getMethodCallsForBucket("getMarket", u.getMinuteTime()) >= Constants.MARKET_THROTTLE) {
            Display.println("Number of Calls for getMarket this minute (" + u.getMinuteTime()
                    + ") is about to exceed the Threshold of " + Constants.MARKET_THROTTLE
                    + " calls per minute. Sleeping for 1 minute.");
            Thread.sleep(60000);
        }

        return ExchangeAPI.getMarket(exchange, apiContext, marketId);
    }

    private static String getFixtureForDayOffset(int offset) {
        return Constants.FIXTURES + findDayOffsetFixtureString(offset);
    }

    private static BFEvent[] getBfEvents(int eventId) throws Exception {
        GetEventsResp resp = GlobalAPI.getEvents(apiContext, eventId);
        BFEvent[] events = resp.getEventItems().getBFEvent();
        if (events == null) {
            events = new BFEvent[]{};
        } else {
            // The API returns Coupons as event names, but Coupons don't contain markets so we remove any
            // events that are Coupons.
            ArrayList<BFEvent> nonCouponEvents = new ArrayList<BFEvent>(events.length);
            for (BFEvent e : events) {
                if (!e.getEventName().equals("Coupons")) {
                    nonCouponEvents.add(e);
                }
            }
            events = nonCouponEvents.toArray(new BFEvent[nonCouponEvents.size()]);
        }
        return events;
    }

    private static MarketSummary[] getMarketSummaryForEvent(int eventId) throws Exception {
        GetEventsResp resp = GlobalAPI.getEvents(apiContext, eventId);
        MarketSummary[] mkts = resp.getMarketItems().getMarketSummary();
        if (mkts == null) {
            mkts = new MarketSummary[]{};
        }
        return mkts;
    }

    // Retrieve and view information about the selected market
    private static void viewMarket(BFEvent event, MarketDetail marketDetail, InflatedMarketPrices prices) throws Exception {
        Display.showMarket(marketDetail.getExchange(), marketDetail.getMarket(), prices);
        Display.println("Correct Score for " + event.getEventName() + " is: " + CorrectScore.findCorrectScoreFromMarketOdds(prices, marketDetail.getMarket()));
    }

    // Retrieve and view information about the selected market
    private static void viewCorrectScore(MarketDetail marketDetail, InflatedMarketPrices prices) throws Exception {
        long startTime = marketDetail.getMarket().getMarketTime().getTimeInMillis();
        long difference = Calendar.getInstance().getTimeInMillis() - startTime;
        difference = difference / (1000 * 60);
        Display.println("Correct Score for " + marketDetail.getEvent().getEventName() + " is: " + CorrectScore.findCorrectScoreFromMarketOdds(prices, marketDetail.getMarket()) + " " + difference + " minutes since start of Game.");
    }

    // Retrieve and view information about the selected market
    private static InflatedMarketPrices getPrices(MarketDetail marketDetail) throws Exception {
        InflatedMarketPrices prices = ExchangeAPI.getMarketPrices(marketDetail.getExchange(), apiContext, marketDetail.getMarket().getMarketId());

        return prices;
    }

    private static void viewCompleteMarket() throws Exception {
        if (isMarketSelected()) {
            InflatedCompleteMarketPrices prices = ExchangeAPI.getCompleteMarketPrices(selectedExchange, apiContext, selectedMarket.getMarketId());

            // Now show the inflated compressed complete market prices.
            Display.showCompleteMarket(selectedExchange, selectedMarket, prices);
        }
    }

    // show all my matched and unmatched bets specified market.
    private static void manageBets() throws Exception {
        if (isMarketSelected()) {
            boolean finished = false;
            while (!finished) {
                // show current bets
                MUBet[] bets = ExchangeAPI.getMUBets(selectedExchange, apiContext, selectedMarket.getMarketId());
                Display.showBets(selectedMarket, bets);

                int choice = Display.getChoiceAnswer("Choose an operation", BETS_MENU);
                switch (choice) {
                    case 0: // Place Bet
                        placeBet();
                        break;
                    case 1: // Update Bet
                        updateBet(bets[Display.getIntAnswer("Choose a bet:", 1, bets.length) - 1]);
                        break;
                    case 2: // Cancel Bet
                        cancelBet(bets[Display.getIntAnswer("Choose a bet:", 1, bets.length) - 1]);
                        break;
                    case 3: // Back
                        finished = true;
                        break;
                }
            }
        }
    }

    // Place a bet on the specified market.
    private static void placeBet() throws Exception {
        if (isMarketSelected()) {
            Runner[] runners = selectedMarket.getRunners().getRunner();
            int choice = Display.getChoiceAnswer("Choose a Runner:", runners);

            // Set up the individual bet to be placed
            PlaceBets bet = new PlaceBets();
            bet.setMarketId(selectedMarket.getMarketId());
            bet.setSelectionId(runners[choice].getSelectionId());
            bet.setBetCategoryType(BetCategoryTypeEnum.E);
            bet.setBetPersistenceType(BetPersistenceTypeEnum.NONE);
            bet.setBetType(BetTypeEnum.Factory.fromValue(Display.getStringAnswer("Bet type:")));
            bet.setPrice(Display.getDoubleAnswer("Price:", false));
            bet.setSize(Display.getDoubleAnswer("Size:", false));

            if (Display.confirm("This action will actually place a bet on the Betfair exchange")) {
                // We can ignore the array here as we only sent in one bet.
                PlaceBetsResult betResult = ExchangeAPI.placeBets(selectedExchange, apiContext, new PlaceBets[]{bet})[0];

                if (betResult.getSuccess()) {
                    Display.println("Bet " + betResult.getBetId() + " placed. " + betResult.getSizeMatched() + " matched @ " + betResult.getAveragePriceMatched());
                } else {
                    Display.println("Failed to place bet: Problem was: " + betResult.getResultCode());
                }
            }
        }
    }

    // Place a bet on the specified market.
    private static void updateBet(MUBet bet) throws Exception {
        if (isMarketSelected()) {
            double newPrice = Display.getDoubleAnswer("New Price [Unchanged - " + bet.getPrice() + "]:", true);
            double newSize = Display.getDoubleAnswer("New Size [Unchanged - " + bet.getSize() + "]:", true);

            if (newPrice == 0.0d) {
                newPrice = bet.getPrice();
            }
            if (newSize == 0.0d) {
                newSize = bet.getSize();
            }

            // Set up the individual bet to be edited
            UpdateBets upd = new UpdateBets();
            upd.setBetId(bet.getBetId());
            upd.setOldBetPersistenceType(bet.getBetPersistenceType());
            upd.setOldPrice(bet.getPrice());
            upd.setOldSize(bet.getSize());
            upd.setNewBetPersistenceType(bet.getBetPersistenceType());
            upd.setNewPrice(newPrice);
            upd.setNewSize(newSize);

            if (Display.confirm("This action will actually edit a bet on the Betfair exchange")) {
                // We can ignore the array here as we only sent in one bet.
                UpdateBetsResult betResult = ExchangeAPI.updateBets(selectedExchange, apiContext, new UpdateBets[]{upd})[0];

                if (betResult.getSuccess()) {
                    Display.println("Bet " + betResult.getBetId() + " updated. New bet is " + betResult.getNewSize() + " @ " + betResult.getNewPrice());
                } else {
                    Display.println("Failed to update bet: Problem was: " + betResult.getResultCode());
                }
            }
        }
    }

    // Place a bet on the specified market.
    private static void cancelBet(MUBet bet) throws Exception {
        if (isMarketSelected()) {
            if (Display.confirm("This action will actually cancel a bet on the Betfair exchange")) {
                CancelBets canc = new CancelBets();
                canc.setBetId(bet.getBetId());

                // We can ignore the array here as we only sent in one bet.
                CancelBetsResult betResult = ExchangeAPI.cancelBets(selectedExchange, apiContext, new CancelBets[]{canc})[0];

                if (betResult.getSuccess()) {
                    Display.println("Bet " + betResult.getBetId() + " cancelled.");
                } else {
                    Display.println("Failed to cancel bet: Problem was: " + betResult.getResultCode());
                }
            }
        }
    }
}
